/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.dx.dex.cf;

import java.util.ArrayList;

import com.android.dx.cf.attrib.AttAnnotationDefault;
import com.android.dx.cf.attrib.AttEnclosingMethod;
import com.android.dx.cf.attrib.AttExceptions;
import com.android.dx.cf.attrib.AttInnerClasses;
import com.android.dx.cf.attrib.AttRuntimeInvisibleAnnotations;
import com.android.dx.cf.attrib.AttRuntimeInvisibleParameterAnnotations;
import com.android.dx.cf.attrib.AttRuntimeVisibleAnnotations;
import com.android.dx.cf.attrib.AttRuntimeVisibleParameterAnnotations;
import com.android.dx.cf.attrib.AttSignature;
import com.android.dx.cf.attrib.InnerClassList;
import com.android.dx.cf.direct.DirectClassFile;
import com.android.dx.cf.iface.AttributeList;
import com.android.dx.cf.iface.Method;
import com.android.dx.cf.iface.MethodList;
import com.android.dx.dex.file.AnnotationUtils;
import com.android.dx.rop.annotation.Annotation;
import com.android.dx.rop.annotation.AnnotationVisibility;
import com.android.dx.rop.annotation.Annotations;
import com.android.dx.rop.annotation.AnnotationsList;
import com.android.dx.rop.annotation.NameValuePair;
import com.android.dx.rop.code.AccessFlags;
import com.android.dx.rop.cst.CstMethodRef;
import com.android.dx.rop.cst.CstNat;
import com.android.dx.rop.cst.CstType;
import com.android.dx.rop.type.StdTypeList;
import com.android.dx.rop.type.Type;
import com.android.dx.rop.type.TypeList;
import com.android.dx.util.Warning;

/**
 * Utility methods that translate various classfile attributes into forms
 * suitable for use in creating {@code dex} files.
 */
/* package */class AttributeTranslator {

	/**
	 * This class is uninstantiable.
	 */
	private AttributeTranslator() {
		// This space intentionally left blank.
	}

	/**
	 * Gets the list of thrown exceptions for a given method.
	 * 
	 * @param method
	 *            {@code non-null;} the method in question
	 * @return {@code non-null;} the list of thrown exceptions
	 */
	public static TypeList getExceptions(Method method) {
		AttributeList attribs = method.getAttributes();
		AttExceptions exceptions = (AttExceptions) attribs
				.findFirst(AttExceptions.ATTRIBUTE_NAME);

		if (exceptions == null) {
			return StdTypeList.EMPTY;
		}

		return exceptions.getExceptions();
	}

	/**
	 * Gets the annotations out of a given {@link AttributeList}. This combines
	 * both visible and invisible annotations into a single result set and also
	 * adds in a system annotation for the {@code Signature} attribute if
	 * present.
	 * 
	 * @param attribs
	 *            {@code non-null;} the attributes list to search in
	 * @return {@code non-null;} the set of annotations, which may be empty
	 */
	public static Annotations getAnnotations(AttributeList attribs) {
		Annotations result = getAnnotations0(attribs);
		Annotation signature = getSignature(attribs);

		if (signature != null) {
			result = Annotations.combine(result, signature);
		}

		return result;
	}

	/**
	 * Gets the annotations out of a given class, similar to
	 * {@link #getAnnotations}, also including annotations for translations of
	 * class-level attributes {@code EnclosingMethod} and {@code InnerClasses},
	 * if present. Additionally, if the class is an annotation class, then this
	 * also includes a representation of all the {@code AnnotationDefault}
	 * values.
	 * 
	 * @param cf
	 *            {@code non-null;} the class in question
	 * @param args
	 *            {@code non-null;} the high-level options
	 * @return {@code non-null;} the set of annotations, which may be empty
	 */
	public static Annotations getClassAnnotations(DirectClassFile cf,
			CfOptions args) {
		CstType thisClass = cf.getThisClass();
		AttributeList attribs = cf.getAttributes();
		Annotations result = getAnnotations(attribs);
		Annotation enclosingMethod = translateEnclosingMethod(attribs);

		try {
			Annotations innerClassAnnotations = translateInnerClasses(
					thisClass, attribs, enclosingMethod == null);
			if (innerClassAnnotations != null) {
				result = Annotations.combine(result, innerClassAnnotations);
			}
		} catch (Warning warn) {
			args.warn.println("warning: " + warn.getMessage());
		}

		if (enclosingMethod != null) {
			result = Annotations.combine(result, enclosingMethod);
		}

		if (AccessFlags.isAnnotation(cf.getAccessFlags())) {
			Annotation annotationDefault = translateAnnotationDefaults(cf);
			if (annotationDefault != null) {
				result = Annotations.combine(result, annotationDefault);
			}
		}

		return result;
	}

	/**
	 * Gets the annotations out of a given method, similar to
	 * {@link #getAnnotations}, also including an annotation for the translation
	 * of the method-specific attribute {@code Exceptions}.
	 * 
	 * @param method
	 *            {@code non-null;} the method in question
	 * @return {@code non-null;} the set of annotations, which may be empty
	 */
	public static Annotations getMethodAnnotations(Method method) {
		Annotations result = getAnnotations(method.getAttributes());
		TypeList exceptions = getExceptions(method);

		if (exceptions.size() != 0) {
			Annotation throwsAnnotation = AnnotationUtils
					.makeThrows(exceptions);
			result = Annotations.combine(result, throwsAnnotation);
		}

		return result;
	}

	/**
	 * Helper method for {@link #getAnnotations} which just gets the existing
	 * annotations, per se.
	 * 
	 * @param attribs
	 *            {@code non-null;} the attributes list to search in
	 * @return {@code non-null;} the set of annotations, which may be empty
	 */
	private static Annotations getAnnotations0(AttributeList attribs) {
		AttRuntimeVisibleAnnotations visible = (AttRuntimeVisibleAnnotations) attribs
				.findFirst(AttRuntimeVisibleAnnotations.ATTRIBUTE_NAME);
		AttRuntimeInvisibleAnnotations invisible = (AttRuntimeInvisibleAnnotations) attribs
				.findFirst(AttRuntimeInvisibleAnnotations.ATTRIBUTE_NAME);

		if (visible == null) {
			if (invisible == null) {
				return Annotations.EMPTY;
			}
			return invisible.getAnnotations();
		}

		if (invisible == null) {
			return visible.getAnnotations();
		}

		// Both are non-null, so combine them.

		return Annotations.combine(visible.getAnnotations(),
				invisible.getAnnotations());
	}

	/**
	 * Gets the {@code Signature} attribute out of a given {@link AttributeList}
	 * , if any, translating it to an annotation.
	 * 
	 * @param attribs
	 *            {@code non-null;} the attributes list to search in
	 * @return {@code null-ok;} the converted {@code Signature} annotation, if
	 *         there was an attribute to translate
	 */
	private static Annotation getSignature(AttributeList attribs) {
		AttSignature signature = (AttSignature) attribs
				.findFirst(AttSignature.ATTRIBUTE_NAME);

		if (signature == null) {
			return null;
		}

		return AnnotationUtils.makeSignature(signature.getSignature());
	}

	/**
	 * Gets the {@code EnclosingMethod} attribute out of a given
	 * {@link AttributeList}, if any, translating it to an annotation. If the
	 * class really has an enclosing method, this returns an
	 * {@code EnclosingMethod} annotation; if not, this returns an
	 * {@code EnclosingClass} annotation.
	 * 
	 * @param attribs
	 *            {@code non-null;} the attributes list to search in
	 * @return {@code null-ok;} the converted {@code EnclosingMethod} or
	 *         {@code EnclosingClass} annotation, if there was an attribute to
	 *         translate
	 */
	private static Annotation translateEnclosingMethod(AttributeList attribs) {
		AttEnclosingMethod enclosingMethod = (AttEnclosingMethod) attribs
				.findFirst(AttEnclosingMethod.ATTRIBUTE_NAME);

		if (enclosingMethod == null) {
			return null;
		}

		CstType enclosingClass = enclosingMethod.getEnclosingClass();
		CstNat nat = enclosingMethod.getMethod();

		if (nat == null) {
			/*
			 * Dalvik doesn't use EnclosingMethod annotations unless there
			 * really is an enclosing method. Anonymous classes are
			 * unambiguously identified by having an InnerClass annotation with
			 * an empty name along with an appropriate EnclosingClass.
			 */
			return AnnotationUtils.makeEnclosingClass(enclosingClass);
		}

		return AnnotationUtils.makeEnclosingMethod(new CstMethodRef(
				enclosingClass, nat));
	}

	/**
	 * Gets the {@code InnerClasses} attribute out of a given
	 * {@link AttributeList}, if any, translating it to one or more of an
	 * {@code InnerClass}, {@code EnclosingClass}, or {@code MemberClasses}
	 * annotation.
	 * 
	 * @param thisClass
	 *            {@code non-null;} type representing the class being processed
	 * @param attribs
	 *            {@code non-null;} the attributes list to search in
	 * @param needEnclosingClass
	 *            whether to include an {@code EnclosingClass} annotation
	 * @return {@code null-ok;} the converted list of annotations, if there was
	 *         an attribute to translate
	 */
	private static Annotations translateInnerClasses(CstType thisClass,
			AttributeList attribs, boolean needEnclosingClass) {
		AttInnerClasses innerClasses = (AttInnerClasses) attribs
				.findFirst(AttInnerClasses.ATTRIBUTE_NAME);

		if (innerClasses == null) {
			return null;
		}

		/*
		 * Search the list for the element representing the current class as
		 * well as for any named member classes.
		 */

		InnerClassList list = innerClasses.getInnerClasses();
		int size = list.size();
		InnerClassList.Item foundThisClass = null;
		ArrayList<Type> membersList = new ArrayList<Type>();

		for (int i = 0; i < size; i++) {
			InnerClassList.Item item = list.get(i);
			CstType innerClass = item.getInnerClass();
			if (innerClass.equals(thisClass)) {
				foundThisClass = item;
			} else if (thisClass.equals(item.getOuterClass())) {
				membersList.add(innerClass.getClassType());
			}
		}

		int membersSize = membersList.size();

		if ((foundThisClass == null) && (membersSize == 0)) {
			return null;
		}

		Annotations result = new Annotations();

		if (foundThisClass != null) {
			result.add(AnnotationUtils.makeInnerClass(
					foundThisClass.getInnerName(),
					foundThisClass.getAccessFlags()));
			if (needEnclosingClass) {
				CstType outer = foundThisClass.getOuterClass();
				if (outer == null) {
					throw new Warning("Ignoring InnerClasses attribute for an "
							+ "anonymous inner class\n" + "("
							+ thisClass.toHuman()
							+ ") that doesn't come with an\n"
							+ "associated EnclosingMethod attribute. "
							+ "This class was probably produced by a\n"
							+ "compiler that did not target the modern "
							+ ".class file format. The recommended\n"
							+ "solution is to recompile the class from "
							+ "source, using an up-to-date compiler\n"
							+ "and without specifying any \"-target\" type "
							+ "options. The consequence of ignoring\n"
							+ "this warning is that reflective operations "
							+ "on this class will incorrectly\n"
							+ "indicate that it is *not* an inner class.");
				}
				result.add(AnnotationUtils.makeEnclosingClass(foundThisClass
						.getOuterClass()));
			}
		}

		if (membersSize != 0) {
			StdTypeList typeList = new StdTypeList(membersSize);
			for (int i = 0; i < membersSize; i++) {
				typeList.set(i, membersList.get(i));
			}
			typeList.setImmutable();
			result.add(AnnotationUtils.makeMemberClasses(typeList));
		}

		result.setImmutable();
		return result;
	}

	/**
	 * Gets the parameter annotations out of a given method. This combines both
	 * visible and invisible annotations into a single result set.
	 * 
	 * @param method
	 *            {@code non-null;} the method in question
	 * @return {@code non-null;} the list of annotation sets, which may be empty
	 */
	public static AnnotationsList getParameterAnnotations(Method method) {
		AttributeList attribs = method.getAttributes();
		AttRuntimeVisibleParameterAnnotations visible = (AttRuntimeVisibleParameterAnnotations) attribs
				.findFirst(AttRuntimeVisibleParameterAnnotations.ATTRIBUTE_NAME);
		AttRuntimeInvisibleParameterAnnotations invisible = (AttRuntimeInvisibleParameterAnnotations) attribs
				.findFirst(AttRuntimeInvisibleParameterAnnotations.ATTRIBUTE_NAME);

		if (visible == null) {
			if (invisible == null) {
				return AnnotationsList.EMPTY;
			}
			return invisible.getParameterAnnotations();
		}

		if (invisible == null) {
			return visible.getParameterAnnotations();
		}

		// Both are non-null, so combine them.

		return AnnotationsList.combine(visible.getParameterAnnotations(),
				invisible.getParameterAnnotations());
	}

	/**
	 * Gets the {@code AnnotationDefault} attributes out of a given class, if
	 * any, reforming them as an {@code AnnotationDefault} annotation.
	 * 
	 * @param cf
	 *            {@code non-null;} the class in question
	 * @return {@code null-ok;} an appropriately-constructed
	 *         {@code AnnotationDefault} annotation, if there were any
	 *         annotation defaults in the class, or {@code null} if not
	 */
	private static Annotation translateAnnotationDefaults(DirectClassFile cf) {
		CstType thisClass = cf.getThisClass();
		MethodList methods = cf.getMethods();
		int sz = methods.size();
		Annotation result = new Annotation(thisClass,
				AnnotationVisibility.EMBEDDED);
		boolean any = false;

		for (int i = 0; i < sz; i++) {
			Method one = methods.get(i);
			AttributeList attribs = one.getAttributes();
			AttAnnotationDefault oneDefault = (AttAnnotationDefault) attribs
					.findFirst(AttAnnotationDefault.ATTRIBUTE_NAME);

			if (oneDefault != null) {
				NameValuePair pair = new NameValuePair(one.getNat().getName(),
						oneDefault.getValue());
				result.add(pair);
				any = true;
			}
		}

		if (!any) {
			return null;
		}

		result.setImmutable();
		return AnnotationUtils.makeAnnotationDefault(result);
	}
}
